table = require 'table'
local Box = {}
Box.__index = Box

function Box.new()
    local o = {}
    o.data = {
        {0, 0, 0, 0},
        {0, 0, 0, 0},
        {0, 0, 0, 0},
        {0, 0, 0, 0},
    }
    o.row = 4
    o.col = 4
    o.success = false
    o.fail = false
    o.goal = 2048
    o.moves = 0
    o.score = 0
    setmetatable(o, Box)
    math.randomseed(tonumber(tostring(os.time()):reverse():sub(1, 7)))
    return o
end

function Box:getRow(r)
    local result = nil
    if r > 0 and r <= self.row then
        result = {}
        local i = 1
        while i <= self.col do
            result[#result+1] = self.data[r][i]
            i = i + 1
        end
    end
    return result
end

function Box:setRow(r, data)
    local result = nil
    if r > 0 and r <= self.row then
        result = {}
        local i = 1
        while i <= self.col do
            self.data[r][i] = data[i]
            i = i + 1
        end
    end
    return result
end

function Box:getCol(c)
    local result = nil
    if c > 0 and c <= self.row then
        result = {}
        local i = 1
        while i <= self.row do
            result[#result+1] = self.data[i][c]
            i = i + 1
        end
    end
    return result
end

function Box:setCol(c, data)
    local result = nil
    if c > 0 and c <= self.row then
        result = {}
        local i = 1
        while i <= self.row do
            self.data[i][c] = data[i]
            i = i + 1
        end
    end
    return result
end

function Box:emptyPos()
    local result = {}
    local i = 1
    while i <= self.row do
        local j = 1
        while j <= self.col do
            if self.data[i][j] == 0 then
                result[#result+1] = {i, j}
            end
            j = j + 1
        end
        i = i + 1
    end
    return result
end

function Box:update(n)
    if n == nil then
        n = 1
    end

    local emptyPos = self:emptyPos()
    if #emptyPos < n then
        self.fail = self:checkFail()
        return
    end

    while n > 0 do
        local i = math.random(1, #emptyPos)
        local p = emptyPos[i]
        self.data[p[1]][p[2]] = 2
        table.remove(emptyPos, i)
        n = n - 1
    end
end

function Box:left()
    local origin = self:copyData()
    for i = 1, self.row, 1 do
        local row = self:getRow(i)
        row = self:mergeEqual(row)
        self:setRow(i, row)
    end
    self:checkUpdate(origin)
end

function Box:right()
    local origin = self:copyData()
    for i = 1, self.row, 1 do
        local row = self:getRow(i)
        row = self:mergeEqual(self:reverseArray(row))
        self:setRow(i, self:reverseArray(row))
    end
    self:checkUpdate(origin)
end

function Box:up()
    local origin = self:copyData()
    for i = 1, self.col, 1 do
        local col = self:getCol(i)
        col = self:mergeEqual(col)
        self:setCol(i, col)
    end 
    self:checkUpdate(origin)
end

function Box:down()
    local origin = self:copyData()
    for i = 1, self.col, 1 do
        local col = self:getCol(i)
        col = self:mergeEqual(self:reverseArray(col))
        self:setCol(i, self:reverseArray(col))
    end 
    self:checkUpdate(origin)
end

function Box:reverseArray(data)
    local result = {}
    local i = #data
    while i > 0 do
        result[#result+1] = data[i]
        i = i - 1
    end
    return result
end

function Box:mergeEqual(data)
    local result = {}
    local count = #data
    local i = 1
    while i <= count do
        local a = data[i]
        local step = 1
        if a > 0 then
            local b = 0
            local j = i + 1
            while b == 0 and j <= count do
                b = data[j]
                j = j + 1
            end
            if a == b then
                local s = a + b
                result[#result+1] = s
                step = j - i
                self.score = self.score + s
                if s == self.goal then
                    self.success = true
                end
            else
                result[#result+1] = a
                step = 1
            end
        end
        i = i + step
    end
    i = #result + 1
    while i <= count do
        result[i] = 0
        i = i + 1
    end
    return result
end

function Box:canMerge(data)
    local result = false
    for i=1,#data-1,1 do    
        if data[i] == data[i+1] then
            return true
        end
    end
    return result
end

function Box:checkUpdate(origin)
    if self:dataChanged(origin) then
        self:update()
        self.moves = self.moves + 1
    end
end

function Box:dataChanged(origin)
    for i=1,self.row,1 do
        for j=1,self.col,1 do
            if self.data[i][j] ~= origin[i][j] then
                return true
            end
        end
    end
    return false
end

function Box:copyData()
    local result = {}
    for i=1,self.row,1 do
        local row = {}
        for j=1,self.col,1 do
            row[#row+1] = self.data[i][j]
        end
        result[#result+1] = row
    end
    return result
end

function Box:checkFail()
    local fail = true
    for i=1,self.row,1 do
        if self:canMerge(self:getRow(i)) then
            return false
        end
    end
    for i=1,self.col,1 do
        if self:canMerge(self:getCol(i)) then
            return false
        end
    end
    return fail
end

function Box:show()
    local i = 1
    while i <= self.row do 
        local j = 1
        local line = ''
        while j <= self.col do
            line = line .. self.data[i][j] .. ' '
            j = j+1
        end
        print(line)
        i = i+1
    end
end

function Box:gameOver()
    return self.fail
end

function Box:gameWin()
    return self.success
end

return Box